package utilities;

/*
CREATE TABLE SEQUENCES(
SEQUENCE_ID VARCHAR(30) NOT NULL,
ID INTEGER NOT NULL,
CONSTRAINT PK_SEQUENCE PRIMARY KEY(SEQUENCE_ID)
);
INSERT INTO SEQUENCES VALUES('MY_SEQUENCE', 0); // Para cada tabla...
-HIGH/LOW ID modelo (máscara) de implementación-
*/

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.math.BigInteger;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.Set;
import java.util.HashSet;

/**
 * Created with IntelliJ IDEA.
 * User: Jordi
 * Date: 4/01/13
 * Time: 18:35
 * To change this template use File | Settings | File Templates.
 */
public class ClausObjectesFctr {

    protected String tabla; // obligatorio
    protected String columnaId; // obligatorio
    protected String sequenceNombreColumna; // obligatorio para distintas de externaSequence
    protected int incremento = 10; // cualquier numero superior a cero
    /** No debe usar la misma conexión para el resto de la aplicación sin realizar un commit siempre antes para GeneradorClaves!!! */
    protected Utilitats utilitats;
    protected int maxRepeticiones = 3; // cualquier numero superior a cero
    protected long timeout = 250; // cualquier numero superior a cero
    protected String estadoSQL = "23000"; // SQLSTATE - clave duplicada
    protected int codigoErrorVendedor = Integer.MIN_VALUE;
    protected int nivelAislamiento = - 1;
    protected boolean setNivelAislamiento = false;
    protected Map valoresBajos = Collections.synchronizedMap(new HashMap());
    protected Map valoresAltos = Collections.synchronizedMap(new HashMap());
    protected Class claveClass = Integer.class; // cualquier clase que herede de Number
    protected boolean externaSequence = false; // se usará con ordenes (sequences) reales: Oracle, PostgreSQL, etc...
    protected String select;
    protected String update;
    protected String insert; // sin no existe se crea automaticamente una nueva orden (sequence) bajo petición
    protected boolean crearSequencePorSolicitud = true;
    protected Number valorInicial = new Integer(0); // no se usará con ordenes (sequences) reales
    protected Set sequences;
    protected boolean almacenStatements;
    public ClausObjectesFctr() {}
    public ClausObjectesFctr(int nivelAislamiento) {
        this.nivelAislamiento = nivelAislamiento;
    }
    public ClausObjectesFctr(String tabla, String sequenceNombreColumna, String columnaId, int nivelAislamiento) {
        this.tabla = tabla;
        this.sequenceNombreColumna = sequenceNombreColumna;
        this.columnaId = columnaId;
        this.nivelAislamiento = nivelAislamiento;
    }
    public ClausObjectesFctr(String tabla, String sequenceNombreColumna, String columnaId) {
        this.tabla = tabla;
        this.sequenceNombreColumna = sequenceNombreColumna;
        this.columnaId = columnaId;
    }
    public ClausObjectesFctr(String tabla, String sequenceNombreColumna, String columnaId, int nivelAislamiento, boolean externaSequence) {
        this.tabla = tabla;
        this.sequenceNombreColumna = sequenceNombreColumna;
        this.columnaId = columnaId;
        this.nivelAislamiento = nivelAislamiento;
        this.externaSequence = externaSequence;
    }
    public ClausObjectesFctr(String tabla, String sequenceNombreColumna, String columnaId, boolean externaSequence) {
        this.tabla = tabla;
        this.sequenceNombreColumna = sequenceNombreColumna;
        this.columnaId = columnaId;
        this.externaSequence = externaSequence;
    }
    public void init(){
        if (!externaSequence && this.tabla == null){
            throw new IllegalStateException("Nombre tabla no existe");
        }
        if (!externaSequence && this.sequenceNombreColumna == null){
            throw new IllegalStateException("Nombre columna de orden (sequence) no existe");
        }
        if (this.columnaId == null){
            throw new IllegalStateException("ID de nombre de columna no existe");
        }
        if (utilitats == null){
            throw new IllegalStateException("DBServicios no establecido");
        }
        if (!externaSequence){
// SELECT ID FROM SEQUENCES WHERE SEQUENCE_NAME = ? ${sequence}
            select = "SELECT " + columnaId + " FROM " + tabla + " WHERE " + sequenceNombreColumna + " = ?";
// UPDATE SEQUENCES SET ID = ID + 10 WHERE SEQUENCE_NAME = ? AND ID = ? ${current ID}
            update = "UPDATE " + tabla + " SET " + columnaId + " = " + columnaId + " + " + String.valueOf(incremento) + " WHERE " +
                    sequenceNombreColumna + " = ? AND " + columnaId + " = ?";
            if (crearSequencePorSolicitud){
// INSERT INTO SEQUENCES (SEQUENCE_NAME, ID) VALUES (?, 0) ${sequence}
                insert = "INSERT INTO " + tabla + " (" + sequenceNombreColumna + ", " + columnaId + ") VALUES (?, " + valorInicial.toString() + ")";
            }
            String totalSequences = "SELECT " + sequenceNombreColumna + " FROM " + tabla;
            try{
                sequences = Collections.synchronizedSet(new HashSet(utilitats.getResultadoUnicaColumnaComoLista(totalSequences)));
            }catch (SQLException sqle){
                throw new RuntimeException(sqle);
            }
        }
    }
    public synchronized Number getValorSiguiente(String sequence) throws SQLException{
        if (!externaSequence && this.select == null){
            throw new IllegalStateException("GeneradorClaves no inicializado");
        }
        if (!externaSequence && !sequences.contains(sequence) && crearSequencePorSolicitud){ // nueva orden (sequence)
            Collection colSeq = new ArrayList(1);
            colSeq.add(sequence);
            utilitats.ejecutar(insert, colSeq);
            sequences.add(sequence);
        }
        Object valor = valoresBajos.get(sequenceNombreColumna);
        Number valorAlto = null;
        if (valor == null){
            valor = getSemilla(sequence);
            valoresBajos.put(sequenceNombreColumna, valor);
            if (claveClass.getName().equals("Integer")){
                valorAlto = ((Number) valor).intValue() + incremento;
            }else if (claveClass.getName().equals("Long")){
                valorAlto = ((Number) valor).longValue() + incremento;
            }else if (claveClass.getName().equals("java.math.BigInteger")){
                valorAlto = new BigInteger(valor.toString()).add(new BigInteger(String.valueOf(incremento)));
            }else if (claveClass.getName().equals("java.math.BigDecimal")){
                valorAlto = new BigDecimal(valor.toString()).add(new BigDecimal(String.valueOf(incremento)));
            }else{
                try{
                    valorAlto = new BigDecimal(valor.toString()).add(new BigDecimal(String.valueOf(incremento)));
                    Constructor c = claveClass.getConstructor(String.class);
                    valorAlto = (Number) c.newInstance(valorAlto.toString());
                }catch (InvocationTargetException ite){
                    throw new SQLException(ite.getTargetException().toString());
                }catch (Exception e){
                    throw new SQLException(e.toString());
                }
            }
            valoresAltos.put(sequenceNombreColumna, valorAlto);
        }else{
            if (claveClass.getName().equals("Integer")){
                if (((Number) valor).intValue() < ((Number) valoresAltos.get(sequenceNombreColumna)).intValue() - 1){
                    valor = new Integer(((Number) valor).intValue() + 1);
                    valoresBajos.put(sequenceNombreColumna, valor);
                }else{
                    valor = getSemilla(sequence);
                    valoresBajos.put(sequenceNombreColumna, valor);
                    valoresAltos.put(sequenceNombreColumna, ((Number) valor).intValue() + incremento);
                }
            }else if (claveClass.getName().equals("Long")){
                if (((Number) valor).longValue() < ((Number) valoresAltos.get(sequenceNombreColumna)).longValue() - 1){
                    valor = new Long(((Number) valor).longValue() + 1);
                    valoresBajos.put(sequenceNombreColumna, valor);
                }else{
                    valor = getSemilla(sequence);
                    valoresBajos.put(sequenceNombreColumna, valor);
                    valoresAltos.put(sequenceNombreColumna, ((Number) valor).longValue() + incremento);
                }
            }else if (claveClass.getName().equals("java.math.BigInteger")){
                if (new BigInteger(valor.toString()).compareTo(new BigInteger(valoresAltos.get(sequenceNombreColumna).toString())) == -1){
                    valor = new BigInteger(valor.toString()).add(BigInteger.ONE);
                    valoresBajos.put(sequenceNombreColumna, valor);
                }else{
                    valor = getSemilla(sequence);
                    valoresBajos.put(sequenceNombreColumna, valor);
                    valoresAltos.put(sequenceNombreColumna, new BigInteger(valor.toString()).add(new BigInteger(String.valueOf(incremento))));
                }
            }else if (claveClass.getName().equals("java.math.BigDecimal")){
                if (new BigDecimal(valor.toString()).compareTo(new BigDecimal(valoresAltos.get(sequenceNombreColumna).toString())) == -1){
                    valor = new BigDecimal(valor.toString()).add(BigDecimal.ONE);
                    valoresBajos.put(sequenceNombreColumna, valor);
                }else{
                    valor = getSemilla(sequence);
                    valoresBajos.put(sequenceNombreColumna, valor);
                    valoresAltos.put(sequenceNombreColumna, new BigDecimal(valor.toString()).add(new BigDecimal(String.valueOf(incremento))));
                }
            }else{
                if (new BigDecimal(valor.toString()).compareTo(new BigDecimal(valoresAltos.get(sequenceNombreColumna).toString())) == -1){
                    valor = new BigDecimal(valor.toString()).add(BigDecimal.ONE);
                    try{
                        valor = new BigDecimal(valor.toString()).add(BigDecimal.ONE);
                        Constructor c = claveClass.getConstructor(String.class);
                        valoresBajos.put(sequenceNombreColumna, c.newInstance(valor.toString()));
                    }catch (InvocationTargetException ite){
                        throw new SQLException(ite.getTargetException().toString());
                    }catch (Exception e){
                        throw new SQLException(e.toString());
                    }
                }else{
                    valor = getSemilla(sequence);
                    valoresBajos.put(sequenceNombreColumna, valor);
                    try{
                        Constructor c = claveClass.getConstructor(String.class);
                        BigDecimal temp = new BigDecimal(valor.toString()).add(new BigDecimal(String.valueOf(incremento)));
                        valoresAltos.put(sequenceNombreColumna, c.newInstance(temp.toString()));
                    }catch (InvocationTargetException ite){
                        throw new SQLException(ite.getTargetException().toString());
                    }catch (Exception e){
                        throw new SQLException(e.toString());
                    }
                }
            }
        }
        return (Number) valor;
    }
    public synchronized Number getValorActual(String sequence) throws SQLException{
        if (!externaSequence && this.select == null){
            throw new IllegalStateException("ClausObjectesFctr no disponible");
        }
        if (!externaSequence && !sequences.contains(sequence) && crearSequencePorSolicitud){ // nueva orden (sequence)
            Collection colSeq = new ArrayList(1);
            colSeq.add(sequence);
            utilitats.ejecutar(insert, colSeq);
            sequences.add(sequence);
        }
        Object valorActual;
        try{
            if (externaSequence){
                if (tabla == null || tabla.trim().length() == 0){
                    valorActual = utilitats.getValorUnico("SELECT " + columnaId);
                }else{
                    valorActual = utilitats.getValorUnico("SELECT " + columnaId + " FROM " + tabla);
                }
            }else{
                Collection colSeq = new ArrayList(1);
                colSeq.add(sequence);
                valorActual = utilitats.getValorUnico(select, colSeq);
            }
        }catch(SQLException sqle){
            throw sqle;
        }finally{
            utilitats.release(false);
        }
        return (Number) valorActual;
    }
    protected synchronized Number getSemilla(String sequence) throws SQLException{
        if (!externaSequence && this.select == null){
            throw new IllegalStateException("ClausObjectesFctr no disponible");
        }
        Object id = null;
        boolean conseguido = false;
        if (externaSequence){
            try{
                id = utilitats.getValorUnico("SELECT " + columnaId + " FROM " + tabla);
                conseguido = true;
            }finally{
                utilitats.release(false);
            }
        }else{
            try{
                int contadorRepeticiones = 0;
                if (nivelAislamiento != -1 && !setNivelAislamiento){ // colocarlo solo una vez
                    utilitats.getConnection().setTransactionIsolation(nivelAislamiento);
                    setNivelAislamiento = true;
                }
                utilitats.beginTran();
                while (!conseguido && contadorRepeticiones <= maxRepeticiones){
                    try{
                        Collection colSeq = new ArrayList(1);
                        colSeq.add(sequence);
                        System.out.println("========== colSeq: " + colSeq);
                        id = utilitats.getValorUnico(select, colSeq);
                        colSeq.add(id);
                        utilitats.ejecutar(update, colSeq);
                        conseguido = true;
                        if (claveClass.getName().equals("Integer")){
                            id = new Integer(((Number) id).intValue() + incremento);
                        }else if (claveClass.getName().equals("Long")){
                            id = new Long(((Number) id).intValue() + incremento);
                        }else if (claveClass.getName().equals("java.math.BigInteger")){
                            id = new BigInteger(id.toString()).add(new BigInteger(String.valueOf(incremento)));
                        }else if (claveClass.getName().equals("java.math.BigDecimal")){
                            id = new BigDecimal(id.toString()).add(new BigDecimal(String.valueOf(incremento)));
                        }else{
                            Constructor c = claveClass.getConstructor(String.class);
                            id = c.newInstance(String.valueOf(incremento));
                        }
                        break;
                    } catch (InstantiationException ie) {
                        throw new SQLException(ie.toString());
                    } catch (IllegalAccessException iae) {
                        throw new SQLException(iae.toString());
                    } catch (InvocationTargetException ite) {
                        throw new SQLException(ite.getTargetException().toString());
                    }catch(SQLException sqle){
                        if ((sqle.getSQLState() != null && sqle.getSQLState().equals(estadoSQL)) || sqle.getErrorCode() == codigoErrorVendedor){
// clave duplicada: reintentar
                        }else{
                            throw sqle; // cualquier otro problema
                        }
                    }
                    contadorRepeticiones++;
                    try{
                        wait(timeout);
                    }catch(InterruptedException ie){
                        throw new SQLException("No puede recibir ID único");
                    }
                }
            }catch(NoSuchMethodException nsme){
                throw new SQLException(nsme.getMessage()); // no debería suceder
            }catch(SQLException sqle){
                throw sqle;
            }finally{
                utilitats.commitTran(); // commit debe realizarse en cualquier caso
                utilitats.release(false);
            }
        }
        if (!conseguido){
            throw new SQLException("No puede recibir ID único");
        }
        return (Number) id;
    }
    public String getTabla() {
        return tabla;
    }
    public void setTabla(String tabla) {
        this.tabla = tabla;
    }
    public String getColumnaId() {
        return columnaId;
    }
    public void setColumnaId(String columnaId) {
        this.columnaId = columnaId;
    }
    public int getIncremento() {
        return incremento;
    }
    public void setIncremento(int incremento) {
        this.incremento = incremento;
    }
    public Utilitats getUtilitats() {
        return utilitats;
    }
    public void setUtilitats(Utilitats utilitats) {
        this.utilitats = utilitats;
        this.utilitats.setAlmacenStatements(almacenStatements);
    }
    public int getMaxRepeticiones() {
        return maxRepeticiones;
    }
    public void setMaxRepeticiones(int maxRepeticiones) {
        this.maxRepeticiones = maxRepeticiones;
    }
    public String getEstadoSQL() {
        return estadoSQL;
    }
    public void setEstadoSQL(String estadoSQL) {
        this.estadoSQL = estadoSQL;
    }
    public long getTimeout() {
        return timeout;
    }
    public void setTimeout(long timeout) {
        if (timeout < 0){
            throw new IllegalArgumentException("Timeout debe ser superior a cero");
        }
        this.timeout = timeout;
    }
    public int getNivelAislamiento() {
        return nivelAislamiento;
    }
    public void setNivelAislamiento(int nivelAislamiento) {
        this.nivelAislamiento = nivelAislamiento;
    }
    public String getSequenceNombreColumna() {
        return sequenceNombreColumna;
    }
    public void setSequenceNombreColumna(String sequenceNombreColumna) {
        this.sequenceNombreColumna = sequenceNombreColumna;
    }
    public Class getClaveClass() {
        return claveClass;
    }
    public void setClaveClass(Class claveClass) {
        if(Number.class.isAssignableFrom(claveClass)){
            this.claveClass = claveClass;
        }else{
            throw new IllegalArgumentException("claveClass debe heredar de Number");
        }
        try{
            Constructor c = claveClass.getConstructor(String.class);
            Object obj = c.newInstance("0");
            String valorString = obj.toString();
            if (valorString == null && !(valorString.equals("0") && !(valorString.startsWith("0.")))){
                throw new IllegalArgumentException(claveClass.getName() + ".toString() debe devolver un valor String o Number");
            }
        }catch (InvocationTargetException ite){
            throw new IllegalArgumentException(ite.getTargetException().toString());
        }catch (Exception ex){
            throw new IllegalArgumentException(ex.toString());
        }
    }
    public boolean isExternaSequence() {
        return externaSequence;
    }
    public void setExternaSequence(boolean externaSequence) {
        this.externaSequence = externaSequence;
    }
    public Number getValorInicial() {
        return valorInicial;
    }
    public void setValorInicial(Number valorInicial) {
        this.valorInicial = valorInicial;
    }
    public boolean isAlmacenStatements() {
        return almacenStatements;
    }
    public void setAlmacenStatements(boolean almacenStatements) {
        this.almacenStatements = almacenStatements;
    }
    public boolean isCrearSequencePorSolicitud() {
        return crearSequencePorSolicitud;
    }
    public void setCrearSequencePorSolicitud(boolean crearSequencePorSolicitud) {
        this.crearSequencePorSolicitud = crearSequencePorSolicitud;
    }
    @SuppressWarnings("empty-statement")
    public void reset() throws SQLException{
        tabla = null;
        columnaId = null;
        sequenceNombreColumna = null;
        incremento = 10;
        maxRepeticiones = 3;
        timeout = 250;
        codigoErrorVendedor = Integer.MIN_VALUE;
        nivelAislamiento = - 1;
        setNivelAislamiento = false;
        valoresBajos.clear();
        valoresAltos.clear();
        claveClass = Integer.class;
        externaSequence = false;
        select = null;
        update = null;;
        insert = null;
        valorInicial = new Integer(0);
        almacenStatements = false;
        sequences.clear();
        utilitats.release(true);
    }
}
